/* * Copyright (C) 2013 Anton Tychyna <anton.tychina@gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.caliper.maven; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.net.JarURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.jar.JarFile; import java.util.jar.Manifest; import static com.google.common.base.Strings.isNullOrEmpty; /** * Run Caliper benchmarks. */ @Mojo(name = "run", defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) public class BenchmarkMojo extends AbstractMojo { private static final Joiner JOINER = Joiner.on(','); private static final String CALIPER_GROUP_ID = "com.google.caliper"; private static final String CALIPER_ARTIFACT_ID = "caliper"; private static final String ALLOCATION_INSTRUMENTER_CLASSNAME = "com.google.monitoring.runtime.instrumentation.AllocationInstrumenter"; private static Predicate<Object> CALIPER_PREDICATE = new Predicate<Object>() { @Override public boolean apply(@Nullable Object o) { if (o instanceof Artifact) { Artifact a = (Artifact) o; if ((Artifact.SCOPE_COMPILE.equals(a.getScope()) || Artifact.SCOPE_RUNTIME.equals(a .getScope())) && a.getGroupId().equals(CALIPER_GROUP_ID) && a.getArtifactId().equals(CALIPER_ARTIFACT_ID)) { return true; } } return false; } }; private static Predicate<Object> ALLOCATION_PREDICATE = new Predicate<Object>() { @Override public boolean apply(@Nullable Object o) { if (o instanceof Artifact) { Artifact a = (Artifact) o; if ("jar".equals(a.getType())) { try { JarFile jarFile = null; try { jarFile = new JarFile(a.getFile()); Manifest manifest = jarFile.getManifest(); if ((manifest != null) && ALLOCATION_INSTRUMENTER_CLASSNAME.equals(manifest.getMainAttributes() .getValue("Premain-Class"))) { return true; } } finally { if (jarFile != null) { jarFile.close(); } } } catch (IOException e) { // do nothing } } } return false; } }; @Component private MavenProject project; /** * Where to look for compiled benchmark classes. */ @Parameter(defaultValue = "${project.build.outputDirectory}") protected File benchmarkClassesDirectory; /** * Maximum length of time allowed for a single trial. Use 0 to allow trials to run indefinitely. */ @Parameter(property = "timeLimit") protected String timeLimit; /** * Instead of measuring, execute a single rep for each scenario. */ @Parameter(property = "dryRun") protected boolean dryRun; /** * Fail build if benchmark throws an exception. */ @Parameter(property = "failBuild") protected boolean failBuild; /** * Number of independent trials to peform per benchmark scenario. */ @Parameter(property = "trials") protected Integer trials; /** * List of measuring instruments to use. */ @Parameter(property = "instruments") protected List<String> instruments; /** * A user-friendly string used to identify the run. */ @Parameter(property = "runName") protected String runName; /** * In addition to normal console output, display a raw feed of very detailed information. */ @Parameter(property = "verbose") protected boolean verbose; /** * Location of Caliper's configuration file. */ @Parameter(property = "caliperConfigFile") protected String caliperConfigFile; /** * Location of Caliper's configuration and data directory. */ @Parameter(property = "caliperDirectory") protected String caliperDirectory; /** * Print the effective configuration that will be used by Caliper. */ @Parameter(property = "printConfig") protected boolean printConfig; /** * List of VMs to test on. */ @Parameter(property = "vms") protected List<String> vms; /** * Specifies a value for any property that could otherwise be specified in $HOME/.caliper/config.properties. */ @Parameter protected Map<String, String> properties; /** * Specifies the values to inject into the 'param' field of the benchmark. */ @Parameter protected Map<String, String> params; /** * Benchmarks to include in this run. By default all classes that begin or end with Benchmark are included. */ @Parameter(property = "includes") protected List<String> includes; /** * Benchmarks to exclude from this run. */ @Parameter(property = "excludes") protected List<String> excludes; /** * Run single benchmark specified by regexp. */ @Parameter(property = "benchmark") protected String benchmark; /** * Java agent for allocation instrument. Plugin will look for agent on a classpath if not defined. */ @Parameter(property = "allocationAgentJar") private String allocationAgentJar; protected List<String> getDefaultIncludes() { return Lists.newArrayList("**/*Benchmark.java", "**/Benchmark*.java"); } @SuppressWarnings("unchecked") @Override public void execute() throws MojoExecutionException, MojoFailureException { checkCaliperDependency(); ConsoleLogger console = new ConsoleLogger(System.out); console.info(""); console.info("-------------------------------------------------------"); console.info(" B E N C H M A R K S "); console.info("-------------------------------------------------------"); ClassLoader benchmarkClassloader = getBenchmarkClassloader(); Properties systemProperties = System.getProperties(); String oldClassPath = systemProperties.getProperty("java.class.path"); // set "java.class.path" system property as it's used by Caliper runner if (getLog().isDebugEnabled()) { getLog().debug("Using classpath: " + ClassLoaderUtils.getClassPathString(benchmarkClassloader)); } systemProperties.setProperty("java.class.path", ClassLoaderUtils.getClassPathString(benchmarkClassloader)); List<CaliperBenchmark> benchmarks = getBenchmarks(benchmarkClassloader); if (benchmarks.isEmpty()) { getLog().info("No benchmarks to run"); } BenchmarkRunResult result = new BenchmarkRunResult(); for (CaliperBenchmark benchmark : benchmarks) { console.info("\nRunning " + benchmark); try { String[] commandLineOptions = getCommandLineOptions(); if (getLog().isDebugEnabled()) { getLog().debug("Command line options: " + Joiner.on(' ').join(commandLineOptions)); } benchmark.run(commandLineOptions); result.successes++; } catch (Exception e) { String exception = "Exception was thrown while running " + benchmark; if (failBuild) { getLog().error(exception, e); throw new MojoFailureException(exception); } else { getLog().warn(exception, e); result.failures++; } } } console.info(""); console.info(result); // restore old class path systemProperties.setProperty("java.class.path", oldClassPath); } private void checkCaliperDependency() throws MojoExecutionException { // check Caliper library is available Optional caliper = Iterables.tryFind(project.getArtifacts(), CALIPER_PREDICATE); if (!caliper.isPresent()) { throw dependencyNotFound(CALIPER_GROUP_ID, CALIPER_ARTIFACT_ID); } getLog().debug("Using Caliper library " + caliper.get()); // get java agent for allocation instrument, Caliper won't run without it if (isNullOrEmpty(allocationAgentJar)) { Optional allocation = Iterables.tryFind(project.getArtifacts(), ALLOCATION_PREDICATE); if (!allocation.isPresent()) { throw new IllegalArgumentException("Can't find allocation agent jar on the classpath"); } Artifact a = (Artifact) allocation.get(); allocationAgentJar = a.getFile().getAbsolutePath(); } File agentJar = new File(allocationAgentJar); if (!agentJar.isFile() || !agentJar.canRead()) { throw new IllegalArgumentException("Can't read agent jar " + allocationAgentJar + " (check file exists and its permissions)"); } getLog().debug("Using allocation library " + allocationAgentJar); } protected List<CaliperBenchmark> getBenchmarks(ClassLoader benchmarkClassloader) throws MojoExecutionException { BenchmarkDirectoryScanner scanner = new BenchmarkDirectoryScanner(benchmarkClassesDirectory); List<String> includes = Lists.newArrayList(getDefaultIncludes()); if (this.includes != null) { includes.addAll(this.includes); } scanner.setIncludes(includes); if (excludes != null) { scanner.setExcludes(excludes); } if (benchmark != null) { scanner.setSpecificBenchmarks(ImmutableList.of(benchmark)); } try { BenchmarkScanResult scan = scanner.scan(); return scan.toBenchmarks(benchmarkClassloader); } catch (ClassNotFoundException e) { throw bug(e); } } @SuppressWarnings("unchecked") protected ClassLoader getBenchmarkClassloader() throws MojoExecutionException { try { Collection<String> urls = project.getTestClasspathElements(); URL[] runtimeUrls = new URL[urls.size() + 1]; int i = 0; for (String url : urls) { runtimeUrls[i++] = new File(url).toURI().toURL(); } runtimeUrls[i] = getPathToPluginJar(); return new BenchmarkClassLoader(runtimeUrls, Thread.currentThread().getContextClassLoader()); } catch (MalformedURLException e) { throw bug(e); } catch (DependencyResolutionRequiredException e) { throw bug(e); } catch (IOException e) { throw bug(e); } } private URL getPathToPluginJar() throws IOException { URL url = getClass().getResource(getClass().getSimpleName() + ".class"); if (!"jar".equalsIgnoreCase(url.getProtocol())) throw new IllegalArgumentException("caliper-maven-plugin classes are not in a jar file"); JarURLConnection connection = (JarURLConnection) url.openConnection(); return connection.getJarFileURL(); } protected String[] getCommandLineOptions() { List<String> options = Lists.newArrayList(); if (!isNullOrEmpty(timeLimit)) { options.add("-l" + timeLimit); } if (dryRun) { options.add("-n"); } if (trials != null) { options.add("-t" + trials); } if (!isNullOrEmpty(caliperConfigFile)) { options.add("-c" + caliperConfigFile); } if (!isNullOrEmpty(caliperDirectory)) { options.add("--directory" + caliperDirectory); } if (printConfig) { options.add("-p"); } if (vms != null && !vms.isEmpty()) { options.add("-m" + JOINER.join(vms)); } if (instruments != null && !instruments.isEmpty()) { options.add("-i" + JOINER.join(instruments)); } if (!isNullOrEmpty(runName)) { options.add("-r" + runName); } if (verbose) { options.add("-v"); } if (properties != null && !properties.isEmpty()) { for (Map.Entry<String, String> e : properties.entrySet()) { options.add("-C" + e.getKey() + "=" + e.getValue()); } } options.add("-Cinstrument.allocation.options.allocationAgentJar=" + allocationAgentJar); if (params != null && !params.isEmpty()) { for (Map.Entry<String, String> e : params.entrySet()) { options.add("-D" + e.getKey() + "=" + e.getValue()); } } return options.toArray(new String[options.size()]); } private static MojoExecutionException bug(Exception e) throws MojoExecutionException { return new MojoExecutionException("Please report this problem to caliper-maven-plugin bug tracker", e); } private MojoExecutionException dependencyNotFound(String groupId, String artifactId) throws MojoExecutionException { throw new MojoExecutionException(groupId + ":" + artifactId + " dependency was not found in project \"" + project.getName() + "\" in compile or runtime scopes"); } }